﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Numerics;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;

#if NET8_0_OR_GREATER
[JsonSerializable(typeof(WTelegram.Session))]
[JsonSerializable(typeof(Dictionary<long, WTelegram.UpdateManager.MBoxState>))]
[JsonSerializable(typeof(IDictionary<long, WTelegram.UpdateManager.MBoxState>))]
[JsonSerializable(typeof(System.Collections.Immutable.ImmutableDictionary<long, WTelegram.UpdateManager.MBoxState>))]
internal partial class WTelegramContext : JsonSerializerContext { }
#endif

namespace WTelegram
{
	public static class Helpers
	{
		/// <summary>Callback for logging a line (string) with its associated <see href="https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.loglevel">severity level</see> (int)</summary>
		public static Action<int, string> Log { get; set; } = DefaultLogger;

		/// <summary>For serializing indented Json with fields included</summary>
		public static readonly JsonSerializerOptions JsonOptions = new() { IncludeFields = true, WriteIndented = true,
#if NET8_0_OR_GREATER
			TypeInfoResolver = JsonSerializer.IsReflectionEnabledByDefault ? null : WTelegramContext.Default,
#endif
			IgnoreReadOnlyProperties = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull };

		private static readonly ConsoleColor[] LogLevelToColor = [ ConsoleColor.DarkGray, ConsoleColor.DarkCyan,
			ConsoleColor.Cyan, ConsoleColor.Yellow, ConsoleColor.Red, ConsoleColor.Magenta, ConsoleColor.DarkBlue ];
		private static void DefaultLogger(int level, string message)
		{
			Console.ForegroundColor = LogLevelToColor[level];
			Console.WriteLine(message);
			Console.ResetColor();
		}

		public static V GetOrCreate<K, V>(this Dictionary<K, V> dictionary, K key) where V : new()
			=> dictionary.TryGetValue(key, out V value) ? value : dictionary[key] = new V();

		/// <summary>Get a cryptographic random 64-bit value</summary>
		public static long RandomLong()
		{
#if NETCOREAPP2_1_OR_GREATER
			long value = 0;
			System.Security.Cryptography.RandomNumberGenerator.Fill(System.Runtime.InteropServices.MemoryMarshal.AsBytes(
				System.Runtime.InteropServices.MemoryMarshal.CreateSpan(ref value, 1)));
			return value;
#else
			var span = new byte[8];
			Encryption.RNG.GetBytes(span);
			return BitConverter.ToInt64(span, 0);
#endif
		}

		public static async Task<int> FullReadAsync(this Stream stream, byte[] buffer, int length, CancellationToken ct)
		{
			for (int offset = 0; offset < length;)
			{
#pragma warning disable CA1835
				var read = await stream.ReadAsync(buffer, offset, length - offset, ct);
#pragma warning restore CA1835
				if (read == 0) return offset;
				offset += read;
			}
			return length;
		}

		internal static byte[] ToBigEndian(ulong value) // variable-size buffer
		{
			int i = 1;
			for (ulong temp = value; (temp >>= 8) != 0; ) i++;
			var result = new byte[i];
			for (; --i >= 0; value >>= 8)
				result[i] = (byte)value;
			return result;
		}

		internal static ulong FromBigEndian(byte[] bytes) // variable-size buffer
		{
			if (bytes.Length > 8) throw new ArgumentException($"expected bytes length <= 8 but got {bytes.Length}");
			ulong result = 0;
			foreach (byte b in bytes)
				result = (result << 8) + b;
			return result;
		}

		internal static byte[] To256Bytes(this BigInteger bi)
		{
			var bigEndian = bi.ToByteArray(true, true);
			if (bigEndian.Length == 256) return bigEndian;
			var result = new byte[256];
			bigEndian.CopyTo(result, 256 - bigEndian.Length);
			return result;
		}

		internal static ulong PQFactorize(ulong pq) // ported from https://github.com/tdlib/td/blob/master/tdutils/td/utils/crypto.cpp#L103
		{
			if (pq < 2) return 1;
			var random = new Random();
			ulong g = 0;
			for (int i = 0, iter = 0; i < 3 || iter < 1000; i++)
			{
				ulong q = (ulong)random.Next(17, 32) % (pq - 1);
				ulong x = ((ulong)random.Next() + (ulong)random.Next() << 31) % (pq - 1) + 1;
				ulong y = x;
				int lim = 1 << (Math.Min(5, i) + 18);
				for (int j = 1; j < lim; j++)
				{
					iter++;
					// x = (q + x * x) % pq
					ulong res = q, a = x;
					while (x != 0)
					{
						if ((x & 1) != 0)
							res = (res + a) % pq;
						a = (a + a) % pq;
						x >>= 1;
					}
					x = res;
					ulong z = x < y ? pq + x - y : x - y;
					g = gcd(z, pq);
					if (g != 1)
						break;

					if ((j & (j - 1)) == 0)
						y = x;
				}
				if (g > 1 && g < pq)
					break;
			}
			if (g != 0)
			{
				ulong other = pq / g;
				if (other < g)
					g = other;
			}
			return g;

			static ulong gcd(ulong left, ulong right)
			{
				while (right != 0)
				{
					ulong num = left % right;
					left = right;
					right = num;
				}
				return left;
			}
		}

		public static int MillerRabinIterations { get; set; } = 64; // 64 is OpenSSL default for 2048-bits numbers
		/// <summary>Miller–Rabin primality test</summary>
		/// <param name="n">The number to check for primality</param>
		public static bool IsProbablePrime(this BigInteger n)
		{
			var n_minus_one = n - BigInteger.One;
			if (n_minus_one.Sign <= 0) return false;

			int s;
			var d = n_minus_one;
			for (s = 0; d.IsEven; s++) d >>= 1;

			var bitLen = n.GetBitLength();
			var randomBytes = new byte[bitLen / 8 + 1];
			var lastByteMask = (byte)((1 << (int)(bitLen % 8)) - 1);
			BigInteger a;
			if (MillerRabinIterations < 15) // 15 is the minimum recommended by Telegram
				Log(3, $"MillerRabinIterations ({MillerRabinIterations}) is below the minimal level of safety (15)");
			for (int i = 0; i < MillerRabinIterations; i++)
			{
				do
				{
					Encryption.RNG.GetBytes(randomBytes);
					randomBytes[^1] &= lastByteMask; // we don't want more bits than necessary
					a = new BigInteger(randomBytes);
				}
				while (a < 3 || a >= n_minus_one);
				a--;

				var x = BigInteger.ModPow(a, d, n);
				if (x.IsOne || x == n_minus_one) continue;

				int r;
				for (r = s - 1; r > 0; r--)
				{
					x = BigInteger.ModPow(x, 2, n);
					if (x.IsOne) return false;
					if (x == n_minus_one) break;
				}
				if (r == 0) return false;
			}
			return true;
		}

		internal static readonly byte[] StrippedThumbJPG = // see https://core.telegram.org/api/files#stripped-thumbnails
		[
			0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49,
			0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43, 0x00, 0x28, 0x1c,
			0x1e, 0x23, 0x1e, 0x19, 0x28, 0x23, 0x21, 0x23, 0x2d, 0x2b, 0x28, 0x30, 0x3c, 0x64, 0x41, 0x3c, 0x37, 0x37,
			0x3c, 0x7b, 0x58, 0x5d, 0x49, 0x64, 0x91, 0x80, 0x99, 0x96, 0x8f, 0x80, 0x8c, 0x8a, 0xa0, 0xb4, 0xe6, 0xc3,
			0xa0, 0xaa, 0xda, 0xad, 0x8a, 0x8c, 0xc8, 0xff, 0xcb, 0xda, 0xee, 0xf5, 0xff, 0xff, 0xff, 0x9b, 0xc1, 0xff,
			0xff, 0xff, 0xfa, 0xff, 0xe6, 0xfd, 0xff, 0xf8, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x2b, 0x2d, 0x2d, 0x3c, 0x35,
			0x3c, 0x76, 0x41, 0x41, 0x76, 0xf8, 0xa5, 0x8c, 0xa5, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8,
			0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8,
			0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8,
			0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x00, 0x00, 0x00, 0x03, 0x01, 0x22, 0x00,
			0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00, 0x1f, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01,
			0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
			0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05,
			0x04, 0x04, 0x00, 0x00, 0x01, 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06,
			0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52,
			0xd1, 0xf0, 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28,
			0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53,
			0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75,
			0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96,
			0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6,
			0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6,
			0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, 0xf2, 0xf3, 0xf4,
			0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xc4, 0x00, 0x1f, 0x01, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01,
			0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
			0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00, 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05,
			0x04, 0x04, 0x00, 0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, 0x06, 0x12, 0x41,
			0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33,
			0x52, 0xf0, 0x15, 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 0x26,
			0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a,
			0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74,
			0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94,
			0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4,
			0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4,
			0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf2, 0xf3, 0xf4,
			0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00,
			0x3f, 0x00
		];

		internal static string GetSystemVersion()
		{
			var os = System.Runtime.InteropServices.RuntimeInformation.OSDescription;
			int space = os.IndexOf(' ') + 1, dot = os.IndexOf('.');
			return os[(os.IndexOf(' ', space) < 0 ? 0 : space)..(dot < 0 ? os.Length : dot)];
		}

		internal static string GetAppVersion()
			=> (Assembly.GetEntryAssembly() ?? Array.Find(AppDomain.CurrentDomain.GetAssemblies(), a => a.EntryPoint != null))?.GetName().Version.ToString() ?? "0.0";

		public class IndirectStream(Stream innerStream) : Stream
		{
			public long? ContentLength;
			protected readonly Stream _innerStream = innerStream;
			public override bool CanRead => _innerStream.CanRead;
			public override bool CanSeek => ContentLength.HasValue || _innerStream.CanSeek;
			public override bool CanWrite => _innerStream.CanWrite;
			public override long Length => ContentLength ?? _innerStream.Length;
			public override long Position { get => _innerStream.Position; set => _innerStream.Position = value; }
			public override void Flush() => _innerStream.Flush();
			public override long Seek(long offset, SeekOrigin origin) => _innerStream.Seek(offset, origin);
			public override void SetLength(long value) => _innerStream.SetLength(value);
			public override void Write(byte[] buffer, int offset, int count) => _innerStream.Write(buffer, offset, count);
			public override int Read(byte[] buffer, int offset, int count) => _innerStream.Read(buffer, offset, count);
			protected override void Dispose(bool disposing) => _innerStream.Dispose();
		}
	}

	public class WTException : ApplicationException
	{
		public WTException(string message) : base(message) { }
		public WTException(string message, Exception innerException) : base(message, innerException) { }
	}
}
